package de.dailab.graphworld.bean;

import de.dailab.graphworld.bean.memory.NodeInfo;
import de.dailab.graphworld.bean.utils.BeanUtils;
import de.dailab.graphworld.ontology.ContractNetProtocol;
import de.dailab.graphworld.ontology.GraphWorldException;
import de.dailab.graphworld.ontology.Job;
import de.dailab.graphworld.ontology.JobAssigned;
import de.dailab.graphworld.ontology.Task;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.Proposal;
import de.dailab.graphworld.ontology.Reply;
import de.dailab.graphworld.world.ExtendedGraphWorldAgentBean;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.ActionResult;
import de.dailab.jiactng.agentcore.comm.ICommunicationAddress;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 *
 * @author Kseniya
 */
public class Scout extends Manager {

    // this is introduced only for carrier selection purpose!
    private int maxCapacity;
    private IActionDescription actionExplore;
    private IActionDescription actionMove;
    private final Queue<Node> moves = new LinkedList<>();
    private final List<Job> jobsUnassigned = new LinkedList<>();
    private final List<JobAssigned> jobsInProgress = new LinkedList<>();

    @Override
    public void doStart() {
        super.doStart();
        actionExplore = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_EXPLORE));
        actionMove = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_MOVE));
    }

    @Override
    public void execute() {
        super.execute();
        if (!isInitialisedInWorld()) {
            return;
        }

        // offer the next available job to carriers
        if (!jobsUnassigned.isEmpty()) {
            synchronized (jobsUnassigned) {
                delegateTask(BeanUtils.findBestJob(jobsUnassigned).getTask());
            }
        }
        // do the next move if defined or explore to get the next move
        if (!moves.isEmpty()) {
            move(moves.peek());
        }
    }

    @Override
    public void receiveResult(ActionResult result) {
        String actionName = result.getAction().getName();
        if (result.getFailure() != null) {
            // if move action failed, simply explore again
            if (ExtendedGraphWorldAgentBean.ACTION_MOVE.equals(actionName)) {
                if (result.getFailure() instanceof GraphWorldException) {
                    GraphWorldException failure = (GraphWorldException) result.getFailure();
                    if (failure.getMessage().startsWith("Target position is not a neighbour")) {
                        moves.clear();
                        explore();
                    }
                }
            } else {
                log.error(actionName);
                log.error(result.getFailure());
                log.error("stored current position: " + memory.read(new NodeInfo(null)).getNode().getID());
            }
        } else {
            switch (actionName) {
                case ExtendedGraphWorldAgentBean.ACTION_INIT_TEAM_POSITION:
                    explore();
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_EXPLORE:
                    Set<Node> neighbours = new HashSet<>((Set<Node>) result.getResults()[0]);
                    consumeExplore(neighbours);
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_MOVE:
                    Node movedTo = (Node) result.getResults()[0];
                    moves.poll();
                    memory.update(new NodeInfo(null), new NodeInfo(movedTo));
                    explore();
                    break;
                default:
                    break;
            }
        }
        super.receiveResult(result);
    }

    private void explore() {
        this.invoke(actionExplore, null, this);
    }

    private void queueNextMove(Node node) {
        moves.offer(node);
    }

    private void move(Node target) {
        this.invoke(actionMove, new Serializable[]{target}, this);
    }

    /**
     * should be called after successful explore only. Updates memory, selects
     * next node to move to and queues an appropriate move action.
     *
     * @param explored set of explored nodes
     */
    private void consumeExplore(Set<Node> explored) {
        // update tasks using new information we received
        updateJobs(explored);
        // select next move randomly
        Node selected = selectMoveTarget(explored);
        // queue next move
        queueNextMove(selected);
    }

    /**
     * updates tasks based on passed nodes information. Removes outdated tasks,
     * creates new tasks, updates existing ones with new gold amounts and
     * resorts tasks so the most important ones (with most gold) are delegated
     * first.
     *
     * @param nodes new nodes information
     */
    private void updateJobs(Collection<Node> nodes) {
        List<Node> nodesWithNoJob = new ArrayList<>(nodes.size());
        synchronized (jobsUnassigned) {
            for (Node node : nodes) {
                boolean taskFound = false;
                Iterator<Job> itr = jobsUnassigned.iterator();
                while (itr.hasNext()) {
                    Task nextTask = itr.next().getTask();
                    // a task referring that node was found, must be (eventually) updated
                    if (nextTask.goldNode.getID() == node.getID()) {
                        taskFound = true;
                        // if no gold on node at all, the task should not be delegated anymore
                        if (node.getGold() == 0) {
                            itr.remove();
                        } else {
                            // update the gold amount in task
                            nextTask.goldNode = node;
                        }
                    }
                }
                // no task found, but there is gold on node
                if (!taskFound && node.getGold() > 0) {
                    nodesWithNoJob.add(node);
                }
            }
        }
        // now check if task with this node is already in progress
        synchronized (jobsInProgress) {
            Iterator<Node> itr = nodesWithNoJob.iterator();
            while (itr.hasNext()) {
                Node nextNode = itr.next();
                for (Job job : jobsInProgress) {
                    if (job.getTask().goldNode.getID() == nextNode.getID()) {
                        itr.remove();
                        break;
                    }
                }
            }
        }
        synchronized (jobsUnassigned) {
            for (Node node : nodesWithNoJob) {
                // store the task to delegate it later
                // sort by gold amount to shift more important tasks to the front
                jobsUnassigned.add(new Job(new Task(node)));
            }
        }
    }

    private Node selectMoveTarget(Collection<Node> nodes) {
        Node current = memory.read(new NodeInfo(null)).getNode();
        Iterator<Node> it = nodes.iterator();
        while (it.hasNext()) {
            Node node = it.next();
            if (node.getID() == current.getID()) {
                it.remove();
            }
        }
        return nodes.iterator().next();
    }

    private void delegateTask(Task task) {
        sendMsgGroup(new Reply(task, ContractNetProtocol.CALL_FOR_PROPOSALS), getCnpGroup());
    }

    @Override
    protected void consumeProposal(Proposal proposal, ICommunicationAddress from) {
        // first of all, read all carriers remembered
        Set<ICommunicationAddress> contractors = new HashSet<>(memory.readAllOfType(ICommunicationAddress.class));
        // if no one to delegate task to - break the whole circus immediately
        if (contractors.isEmpty()) {
            return;
        }

        // remember the made proposal for the job
        storeProposalInJob(proposal, from);
        Task task = proposal.getTask();
        List<Job> pendingJobs = BeanUtils.findJobsByTask(jobsUnassigned, task);
        if (pendingJobs.isEmpty()) {
            return;
        }
        Job pendingJob = BeanUtils.findJobsByTask(jobsUnassigned, task).iterator().next();

        // check if we can delegate the job to some carrier now
        if (contractors.equals(pendingJob.getAllCandidates())) {
            // find the best assignee
            ICommunicationAddress winner = selectBestAssignee(pendingJob);
            // no assignee could be selected - probably because all of them refused?...
            // so do nothing else, we will try to call for proposals for this job again in the next execute call
            if (winner == null) {
                return;
            }
            // set job in progress
            synchronized (jobsInProgress) {
                jobsInProgress.add(new JobAssigned(winner, pendingJob.getProposalByAssignee(winner)));
            }
            synchronized (jobsUnassigned) {
                BeanUtils.removeJobsByTask(jobsUnassigned, task);
            }
            // inform the winner
            sendMsgPersonal(new Reply(task, ContractNetProtocol.ACCEPT_PROPOSAL), winner);
            // inform the rest
            contractors.remove(winner);
            for (ICommunicationAddress loser : contractors) {
                sendMsgPersonal(new Reply(task, ContractNetProtocol.REJECT_PROPOSAL), loser);
            }
        }
    }

    @Override
    protected void consumeRefusal(Proposal proposal, ICommunicationAddress from) {
        // first of all, read all carriers remembered
        Set<ICommunicationAddress> contractors = new HashSet<>(memory.readAllOfType(ICommunicationAddress.class));
        // if no one to delegate task to - break the whole circus immediately
        if (contractors.isEmpty()) {
            return;
        }
        // store that the carrier refused to do the job
        storeProposalInJob(proposal, from);
    }

    private void storeProposalInJob(Proposal proposal, ICommunicationAddress from) {
        Task task = proposal.getTask();
        List<Job> pendingJobs = BeanUtils.findJobsByTask(jobsUnassigned, task);
        // job was already assigned
        if (pendingJobs.isEmpty()) {
            return;
        }
        Job pendingJob = pendingJobs.iterator().next();
        // clean up pending jobs if necessary
        // TODO check if necessary now
        if (pendingJobs.size() > 1) {
            pendingJobs.remove(pendingJob);
            synchronized (jobsUnassigned) {
                jobsUnassigned.removeAll(pendingJobs);
            }
        }

        // notice received proposal
        pendingJob.addProposal(from, proposal);
    }

    @Override
    protected void consumeFailure(Task task) {
        // clean up pending jobs
        synchronized (jobsInProgress) {
            BeanUtils.removeJobsByTask(jobsInProgress, task);
        }
    }

    @Override
    protected void consumeResult(Task task) {
        // clean up pending jobs
        synchronized (jobsInProgress) {
            BeanUtils.removeJobsByTask(jobsInProgress, task);
        }
    }

    /**
     * Core functionality - a carrier that suits the job best is selected.
     *
     * @param job to be assigned to a carrier
     * @return the carrier that will get the job
     */
    private ICommunicationAddress selectBestAssignee(Job job) {
        ICommunicationAddress selected = null;
        double maxEfficiency = 0.0;
        for (ICommunicationAddress candidate : job) {
            Proposal proposal = job.getProposalByAssignee(candidate);
            if (proposal.getFreeCapacity() > 0) {
                double efficiency = calcEfficiency(proposal);
                if (maxEfficiency < efficiency) {
                    maxEfficiency = efficiency;
                    selected = candidate;
                }
            }
        }
        return selected;
    }

    private double calcEfficiency(Proposal proposal) {
        return new Double(proposal.getFreeCapacity())
                / new Double(getMaxCapacity())
                / new Double(proposal.getDistance().size());
    }

    /**
     * Get the value of maxCapacity
     *
     * @return the value of maxCapacity
     */
    public int getMaxCapacity() {
        return maxCapacity;
    }

    /**
     * Set the value of maxCapacity
     *
     * @param maxCapacity new value of maxCapacity
     */
    public void setMaxCapacity(int maxCapacity) {
        this.maxCapacity = maxCapacity;
    }
}
